/*
 * Copyright 2009-2010 Belmont Software Services
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.belmont.backup.server;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.belmont.backup.*;
import org.json.*;

public class BackupProtocolServlet extends HttpServlet implements IBackupConstants {
    FileBackupServer fserver;

    public void init(ServletConfig sconfig) throws ServletException {
        super.init(sconfig);
        fserver = BackupService.getInstance().fserver;
    }

    public void destroy() {
        Utils.log(LOG_INFO, "BackupProtocolServlet destroy");
    }
    
    void sendErrorResponse(HttpServletResponse resp, int code, String message, Exception ex) {
        try {
            if (ex == null) {
                Utils.log(LOG_ERROR, "Error response sent "+code+" "+message);
            } else {
                Utils.log(LOG_ERROR, "Error response sent "+code+" "+message, ex);
            }
            if (message != null) {
                resp.sendError(code, message);
            } else if (ex != null) {
                resp.sendError(code, ex.toString());
            } else {
                resp.sendError(code, "Server error.");
            }
        } catch (IOException e) {
            Utils.log(LOG_ERROR, "Servlet exception", ex);
        }
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String pathInfo = req.getPathInfo().toLowerCase();

        try {
            Utils.log(LOG_INFO, "BackupProtocolServlet doGet "+pathInfo);
            //printHeader(req);

            if (pathInfo.startsWith(REQUEST_SERVERLOG, 1)) {
                handleServerLog(req, resp);
                return;
            }

            if (! BackupService.getInstance().isEnabled()) {
                sendErrorResponse(resp, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                                  "Service is disabled", null);
                return;
            }

            if (pathInfo.startsWith(REQUEST_GETVERSIONS, 1)) {
                handleGetVersions(req, resp);
            } else if (pathInfo.startsWith(REQUEST_CONNECT, 1)) {
                handleConnect(req, resp);
            } else if (pathInfo.startsWith(REQUEST_DISCONNECT, 1)) {
                handleDisconnect(req, resp);
            } else if (pathInfo.startsWith(REQUEST_GETFILE, 1)) {
                handleGetFile(req, resp);
            } else if (pathInfo.startsWith(REQUEST_CANCELBACKUP, 1)) {
                handleCancelBackup(req, resp);
            } else if (pathInfo.startsWith(REQUEST_COMMITBACKUP, 1)) {
                handleCommitBackup(req, resp);
            } else if (pathInfo.startsWith(REQUEST_GETCONFIG, 1)) {
                handleGetConfig(req, resp);
            } else if (pathInfo.startsWith(REQUEST_STARTBACKUP, 1)) {
                handleStartBackup(req, resp);
            } else {
                resp.sendError(500,"Unknown directive");
            }
        } catch (JSONException ex) {
            sendErrorResponse(resp, 500, ex.toString(), ex);
        } catch (IOException ex) {
            sendErrorResponse(resp, 500, ex.toString(), ex);
        } finally {
            Utils.log(LOG_INFO, "doGet "+pathInfo+" terminated.");
        }
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        String pathInfo = req.getPathInfo().toLowerCase();

        try {
            Utils.log(LOG_INFO, "BackupProtocolServlet doPost "+pathInfo);
            //printHeader(req);

            if (! BackupService.getInstance().isEnabled()) {
                sendErrorResponse(resp, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                                  "Service is disabled", null);
                return;
            }

            if (pathInfo.startsWith(REQUEST_STARTBACKUP, 1)) {
                handleStartBackup(req, resp);
            } else if (pathInfo.startsWith(REQUEST_SENDFILE, 1)) {
                handleSendFile(req, resp);
            } else if (pathInfo.startsWith(REQUEST_CHECKFILES, 1)) {
                handleCheckFiles(req, resp);
            } else {
                resp.sendError(500,"Unknown directive");
            }
        } catch (JSONException ex) {
            Utils.log(LOG_ERROR, "Servlet exception", ex);
            sendErrorResponse(resp, 500, null, ex);
        } catch (IOException ex) {
            Utils.log(LOG_ERROR, "Servlet exception", ex);
            sendErrorResponse(resp, 500, null, ex);
        } catch (Throwable ex) {
            Utils.log(LOG_ERROR, "Servlet exception", ex);
        } finally {
            Utils.log(LOG_INFO, "doPost "+pathInfo+" terminated.");
        }
    }

    protected void doPut(HttpServletRequest req, HttpServletResponse resp) {
        doPost(req, resp);
    }

    void handleConnect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String host = req.getHeader(REQUEST_PROP_HOST);
        String user = req.getHeader(REQUEST_PROP_USER);
        String uuid = req.getHeader(REQUEST_PROP_UUID);

        if (host == null || "unknown".equals(host)) {
            sendErrorResponse(resp, 500,"No backup host", null);
            return;
        }

        String reply = fserver.connect(req.getHeader(REQUEST_PROP_PROTOCOL), uuid, host,
                                       req.getHeader(REQUEST_PROP_OS),
                                       req.getHeader(REQUEST_PROP_OSVERSION),
                                       user);

        PrintWriter writer = resp.getWriter();
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("text/plain");
        resp.setContentLength(reply.length());
        writer.print(reply);
        writer.flush();
        writer.close();
    }

    void handleDisconnect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String session = getBackupSession(req);
        if (session == null) {
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }

        fserver.disconnect(session);
        String response = "disconnect succesful";
        PrintWriter writer = resp.getWriter();
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("text/plain");
        resp.setContentLength(response.length());
        writer.print(response);
        writer.flush();
        writer.close();
    }

    String getBackupSession(HttpServletRequest req) {
        String session = req.getHeader(REQUEST_PROP_SESSION);

        if (session == null) {
            Hashtable<String, Vector<String>> query = getQueryTable(req.getQueryString());
            Utils.log(LOG_INFO, "QUERY "+query);
            if (query != null && query.get(REQUEST_PROP_SESSION) != null) {
                session = query.get(REQUEST_PROP_SESSION).elementAt(0);
            }
        }
        return session;
    }

    void printHeader(HttpServletRequest req) {
        System.out.println("URL: "+req.getRequestURL());
        Enumeration<String> names = req.getHeaderNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            System.out.println("HEADER: "+name+"="+req.getHeader(name));
        }
    }

    void handleGetVersions(HttpServletRequest req, HttpServletResponse resp) throws IOException, JSONException {
        String session = getBackupSession(req);

        if (session == null) {
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        JSONObject versions = fserver.getAvailableVersions(session);
        if (versions == null) {
            versions = new JSONObject();
        }
        String vs = versions.toString();
        PrintWriter writer = resp.getWriter();
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("application/json");
        resp.setContentLength(vs.length());
        writer.print(vs);
        writer.flush();
        writer.close();
    }

    void handleGetConfig(HttpServletRequest req, HttpServletResponse resp) throws IOException, JSONException {
        String vs = BackupService.getInstance().getConfig().getJSON().toString();
        PrintWriter writer = resp.getWriter();
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("application/json");
        resp.setContentLength(vs.length());
        writer.print(vs);
        writer.flush();
        writer.close();
    }

    void handleGetFile(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String session = getBackupSession(req);
        if (session == null) {
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        try {
            String digest = req.getHeader(REQUEST_PROP_BACKUP_FILE_DIGEST);
            String name = req.getHeader(REQUEST_PROP_BACKUP_FILE_NAME);
            long offset = Long.parseLong(req.getHeader(REQUEST_PROP_BACKUP_FILE_OFFSET));
            long length = Long.parseLong(req.getHeader(REQUEST_PROP_BACKUP_FILE_LENGTH));
            resp.setContentType("application/zip");
            resp.setContentLength((int)(length - offset));
            OutputStream out = resp.getOutputStream();
            try {
                fserver.getFile(session, digest, name, length, offset, out);
            } catch (InterruptedException ex) {
                throw new IOException("Operation interruped");
            } finally {
                out.close();
            }
        } catch (NumberFormatException ex) {
            throw new IOException("Bad number in request");
        }
    }

    void handleCancelBackup(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String session = getBackupSession(req);
        if (session == null) {
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        String path = req.getHeader(REQUEST_PROP_BACKUP_PATH);
        if (path == null) {
            sendErrorResponse(resp, 500, "No backup path specified.", null);
            return;
        }

        fserver.cancelBackup(session, path);
        String response = "cancel succesful";
        PrintWriter writer = resp.getWriter();
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentLength(response.length());
        resp.setContentType("text/plain");
        writer.print(response);
        writer.flush();
        writer.close();
	
    }

    void handleCommitBackup(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String session = getBackupSession(req);
        if (session == null) {
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        String path = req.getHeader(REQUEST_PROP_BACKUP_PATH);
        if (path == null) {
            sendErrorResponse(resp, 500, "No backup path specified.", null);
            return;
        }
        String backupid = req.getHeader(REQUEST_PROP_BACKUP_ID);

        Utils.log(LOG_INFO, "HANDLE COMMIT "+session+" "+path);
        if (backupid == null) {
            sendErrorResponse(resp, 500, "No backupid specified.", null);
            return;
        }
        fserver.commitBackup(session, backupid, path);
        String response = "commit succesful";
        PrintWriter writer = resp.getWriter();
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("text/plain");
        resp.setContentLength(response.length());
        writer.print(response);
        writer.flush();
        writer.close();
    }

    void handleSendFile(HttpServletRequest req, HttpServletResponse resp) throws IOException, JSONException {
        String session = getBackupSession(req);
        if (session == null) {
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        try {
            String digest = req.getHeader(REQUEST_PROP_BACKUP_FILE_DIGEST);
            String name = req.getHeader(REQUEST_PROP_BACKUP_FILE_NAME);
            long length = Long.parseLong(req.getHeader(REQUEST_PROP_BACKUP_FILE_LENGTH));
            long offset = Long.parseLong(req.getHeader(REQUEST_PROP_BACKUP_FILE_OFFSET));
            String cls = req.getHeader(REQUEST_PROP_BACKUP_FILE_COMPRESSED_LENGTH);
            boolean compressed =  cls != null;
            long clength = (cls == null) ? -1 : Long.parseLong(cls);
            InputStream in = req.getInputStream();

            Utils.log(LOG_INFO, "Receive file session "+session+" file "+name+" length "+length+" offset "+offset+" compressed "+compressed+" compressedLength "+clength);
            if (compressed) {
                // XXX: need to pass in compressed length to sendFile in case the
                // compression actually made the file bigger.

                Utils.log(LOG_INFO, "Turning on compression for "+name);
                in = new InflaterInputStream(in);
            }

            if (length > 0) {
                try {
                    fserver.sendFile(session, digest, name, length, offset, in);
                } finally {
                    in.close();
                }
            } else {
                in.close();
            }
            Utils.log(LOG_INFO, "Receive file (done) session "+session+" file "+name);
            String response = name+" received";
            PrintWriter writer = resp.getWriter();
            resp.setStatus(HttpServletResponse.SC_OK);
            resp.setContentType("text/plain");
            resp.setContentLength(response.length());
            writer.print(response);
            writer.flush();
            writer.close();
        } catch (InterruptedException ex) {
            throw new IOException("Operation interrupted.");
        } catch (NumberFormatException ex) {
            throw new IOException("Bad number in request");
        }
    }


    /**
     * Horribly innefficient way of getting the post data as a string.
     */
    private String getPostData(HttpServletRequest req) throws IOException {
        int len = req.getContentLength();
        byte buf[];

        if (len == -1) {
            buf = new byte[1024];
        } else {
            buf = new byte[len];
        }

        StringBuffer sb = new StringBuffer();
        InputStream in = req.getInputStream();

        while ((len = in.read(buf)) != -1) {
            sb.append(new String(buf, 0, len));
        }
        return sb.toString();
    }

    void handleCheckFiles(HttpServletRequest req, HttpServletResponse resp) throws IOException, JSONException {
        String session = getBackupSession(req);

        if (session == null) {
            Utils.log(LOG_INFO, "No backup session specified.");
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        String path = req.getHeader(REQUEST_PROP_BACKUP_PATH);
        String backupid = req.getHeader(REQUEST_PROP_BACKUP_ID);
        JSONObject fileRequest = new JSONObject(getPostData(req));
        JSONObject reply = fserver.checkFiles(session, backupid, path, fileRequest);
        String vs = reply.toString();

        Utils.log(LOG_INFO, "SERVER RESPONSE checkFiles: "+vs.length()+" "+vs);
        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("application/json");
        resp.setContentLength(vs.length());
        resp.flushBuffer();
        DataOutputStream out = new DataOutputStream(resp.getOutputStream());
        out.writeBytes(vs);
        out.flush();	
    }

    void handleStartBackup(HttpServletRequest req, HttpServletResponse resp) throws IOException, JSONException {
        String session = getBackupSession(req);

        if (session == null) {
            Utils.log(LOG_INFO, "No backup session specified.");
            sendErrorResponse(resp, 500, "No backup session specified.", null);
            return;
        }
        String path = req.getHeader(REQUEST_PROP_BACKUP_PATH);
        JSONObject obj = fserver.startBackup(session, path);
        String vs = obj.toString();

        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("application/json");
        resp.setContentLength(vs.length());
        resp.flushBuffer();
        DataOutputStream out = new DataOutputStream(resp.getOutputStream());
        out.writeBytes(vs);
        out.flush();
    }

    private Hashtable<String, Vector<String>> getQueryTable(String queryString) {
        if (queryString == null) {
            return null;
        }

        Hashtable<String, Vector<String>> t = new Hashtable<String, Vector<String>>();
        int idx;

        do {
            idx = queryString.indexOf('&');

            String q = (idx == -1) ? queryString : queryString.substring(0, idx);
            int eq = q.indexOf('=');
            String k = q.substring(0, eq);
            String v = q.substring(eq+1);
            Vector<String> values = t.get(k);

            if (values == null) {
                values = new Vector<String>();
                t.put(k, values);
            }
            values.addElement(v);
            if (idx != -1) {
                queryString = queryString.substring(idx+1);
            }
        } while (idx != -1);

        return t;
    }

    void handleServerLog(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        File f = Utils.getLogFile();

        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("text/plain");

        if (! f.exists() || f.length() == 0) {
            String msg = "No log entries";
            resp.setContentLength(msg.length());
            PrintWriter writer = resp.getWriter();
            writer.print(msg);
            writer.close();
            return;
        } else {
            String msg = "No log entries";
            int flen = (int)f.length();
            resp.setContentLength(flen);
            OutputStream out = resp.getOutputStream();
            try {
                InputStream in = new FileInputStream(f);

                try {
                    int len;
                    byte buf[] = new byte[1024];

                    while ((len = in.read(buf)) != -1) {
                        if (len <= flen) {
                            out.write(buf, 0, len);
                        } else {
                            out.write(buf, 0, flen);
                        }
                        flen -= len;
                    }
                } finally {
                    in.close();
                }
            } finally {
                out.close();
            }
        }
    }
}
